﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using SlimMath;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo ("MyTestProject")]

/**
 * メンバーのCollisionShapeを初期値nullで置き換えを検討する
 * */




namespace M3G {


    public abstract class Node : Transformable {

        #region Constant
        // Target
        public const int None   = 144;
        public const int Origin = 145;
        // Reference Axis
        public const int XAxis  = 146;
        public const int YAxis  = 147;
        public const int ZAxis  = 148;
        #endregion

        #region Field
        protected Node       parent;
        protected List<Node> children;
        bool            renderingEnabled;
        bool            pickingEnabled;
        bool            collisionEnabled;
        float           alphaFactor;
        uint            scope;
        Node            zReference;
        int             zTarget;
        Node            yReference;
        int             yTarget;
        float           lodResolution;
        float[]         boundingBox;
        float[]         boundingSphere;
        CollisionShape  collisionShape;
        #endregion

        #region Method
        public Node () {

            this.parent = null;
            this.children = new List<Node> ();

            this.renderingEnabled = true;
            this.pickingEnabled   = true;
            this.collisionEnabled = true;
            this.alphaFactor = 1.0f;
            this.scope = 0xffffffff;

            this.zTarget = None;
            this.yTarget = None;
            this.zReference = null;
            this.yReference = null;

            this.lodResolution = 0;

            this.boundingBox = null;
            this.boundingSphere = null;
            this.collisionShape = new CollisionShape ();
        }

        protected Node (Node node)
            : base (node) {
            this.parent = node.parent;
            this.children = new List<Node> ();
            foreach (var child in node.children) {
                this.children.Add ((Node)child.Duplicate ());
            }
            this.renderingEnabled = node.renderingEnabled;
            this.pickingEnabled = node.pickingEnabled;
            this.collisionEnabled = node.collisionEnabled;
            this.alphaFactor = node.alphaFactor;
            this.scope = node.scope;

            this.zTarget = node.zTarget;
            this.yTarget = node.yTarget;
            this.zReference = node.zReference;
            this.yReference = node.yReference;

            this.lodResolution = node.lodResolution;


            this.boundingBox    = (node.boundingBox == null) ? null : (float[])node.boundingBox.Clone ();
            this.boundingSphere = (node.boundingSphere == null) ? null : (float[])node.boundingSphere.Clone ();
            this.collisionShape = (node.collisionShape == null) ? null : new CollisionShape (node.collisionShape);
        }


        public Node Parent {
            get { return parent; }
        }

        internal void SetParent (Node node) {
            this.parent = node;
        }

        public bool RenderingEnabled {
            get { return renderingEnabled; }
            set { renderingEnabled = value; }
        }

        public bool PickingEnabled {
            get { return pickingEnabled; }
            set { pickingEnabled = value; }
        }

        public bool CollisionEnabled {
            get { return collisionEnabled; }
            set { collisionEnabled = value; }
        }


        public uint Scope {
            get { return scope; }
            set { scope = value; }
        }



        public float AlphaFactor {
            get { return alphaFactor; }
            set {
                var alphaFactor = value;
                if (alphaFactor < 0 || alphaFactor > 1) {
                    throw new ArgumentException ("Alpha factor must be within [0,1], alpha=" + alphaFactor);
                }

                this.alphaFactor = alphaFactor;
            }
        }

        public float[] BoundingBox {
            get {
                return (boundingBox==null) ? null : (float[])boundingBox.Clone();
            }
        }

        public void SetBoundingBox (float minX, float maxX, float minY, float maxY, float minZ, float maxZ) {
            if (minX > maxX || minY > maxY || minZ > maxZ) {
                throw new ArgumentException ("Bouding box vertices are invlid, minX=" + minX + ",maxX=" + maxX + ",minY=" + minY + ",maxY=" + maxY + ",minZ=" + minZ + ",maxZ=" + maxZ);
            }
            if (minX == maxX && minY == maxY && minZ == maxZ) {
                throw new AggregateException ("Bounding box is zero");
            }

            maxX = (minX == maxX) ? maxX + Mathf.Epsilon : maxX;
            maxY = (minY == maxY) ? maxY + Mathf.Epsilon : maxY;
            maxZ = (minZ == maxZ) ? maxZ + Mathf.Epsilon : maxZ;

            this.boundingBox = new float[] {minX, maxX, minY, maxY, minZ, maxZ};
        }



        public float[] BoundingSphere {
            get {
                return (boundingSphere == null) ? null : (float[])boundingSphere.Clone ();
            }
        }


        public void SetBoundingSphere (float centerX, float centerY, float centerZ, float radius) {
            if (radius < 0) {
                throw new ArgumentException ("Radius must be within [0,inf], r=" + radius);
            }

            this.boundingSphere = new float[] { centerX, centerY, centerZ, radius };
        }

        public virtual void Align (Node reference) {

            // first rotation
            var zRef = (zTarget == Origin) ? (zReference == null) ? reference : zReference : null;
            var yRef = (yTarget == Origin) ? (yReference == null) ? reference : zReference : null;
            var zRot = new Quaternion ();
            var yRot = new Quaternion ();

            if (zRef != null) {
                var mz = zRef.GetTransformTo (this).Matrix;
                var tz = new Vector3 (mz[2], mz[6], mz[10]);
                var az = Vector3.Cross (new Vector3 (0, 0, 1), tz);
                var rz  = Vector3.Dot (new Vector3 (0, 0, 1), tz);
                zRot = Quaternion.RotationAxis (az, rz);
                if (yRef != null) {
                    var my = yRef.GetTransformTo (this).Matrix;
                    var ty = new Vector3 (my[1], my[5], my[9]);
                    var zRotInv = Matrix.RotationQuaternion (zRot);
                    zRotInv.Invert ();
                    var ty2 = Vector3.Transform (ty, zRotInv);
                    var ty3 = new Vector3 (ty2[0], ty2[1], 0);
                    var ay = Vector3.Cross (new Vector3 (0, 1, 0), ty3);
                    var ry  = Vector3.Dot (new Vector3 (0, 1, 0), ty3);
                    yRot = Quaternion.RotationAxis (ay, ry);
                }
            }
            if (zRef == null && yRef != null) {
                var tm = zRef.GetTransformTo (this).Matrix;
                var ty = new Vector3 (tm[1], tm[5], tm[9]);
                var ay = Vector3.Cross (new Vector3 (0, 1, 0), ty);
                var ry  = Vector3.Dot (new Vector3 (0, 1, 0), ty);
                yRot = Quaternion.RotationAxis (ay, ry);
            }

            var q = zRot * yRot;
            SetOrientationQuat (q[0], q[1], q[2], q[3]);
        }

        public Dictionary<int, Node> AlignmentReference {
            get {
                var dic = new Dictionary<int, Node> (2);
                dic.Add (ZAxis, zReference);
                dic.Add (YAxis, yReference);
                return dic;
            }
        }

        public Dictionary<int, int> AlignmentTarget {
            get {
                var dic = new Dictionary<int, int> (2);
                dic.Add (ZAxis, zTarget);
                dic.Add (YAxis, yTarget);
                return dic;
            }
        }


        public void SetAlignment (Node zRef, int zTarget, Node yRef, int yTarget) {
            if (zTarget < None || zTarget > Origin || yTarget < None || yTarget > Origin) {
                throw new ArgumentException ("Target must be None,Origin, z=" + zTarget + ",y=" + yTarget);
            }
            if (zRef == yRef && zTarget == yTarget && zTarget != None) {
                throw new ArgumentException ("Can't align to same node in both ZAxis and YAxis");
            }
            if (zRef == this || yRef == this) {
                throw new ArgumentException ("Can't align to myself");
            }

            this.zReference = zRef;
            this.yReference = yRef;
            this.zTarget = zTarget;
            this.yTarget = yTarget;
        }

        public int GetCollisionShape (float[] min, float[] max) {
            if (min == null && max == null) {
                return collisionShape.Orientations;
            }
            int rank = collisionShape.Rank;
            if (min.Length < rank || max.Length < rank) {
                throw new ArgumentException ("Length of Min or Max is less than required, min=" + min.Length + ",max=" + max.Length + ",req=" + rank);
            }

            Array.Copy (collisionShape.Min, min, rank);
            Array.Copy (collisionShape.Max, max, rank);

            return collisionShape.Orientations;
        }

        public void SetCollisionShape (int orientations, float[] min, float[] max) {
            if (min == null || max == null) {
                throw new ArgumentNullException ("Min or Max is null");
            }
            for (var i=0; i < 13; i++) {
                if ((orientations & (1 << i)) != 0) {
                    if (i > min.Length - 1 || i > max.Length - 1) {
                        throw new ArgumentException ("Length of Min or Max is less than required");
                    }
                    if (min[i] > max[i]) {
                        throw new ArgumentException ("Min and Max values must be Min<Max, min=" + min[i] + ",max=" + max[i]);
                    }
                }
            }

            this.collisionShape.SetKDOP (orientations, min, max);
            this.collisionShape.GenerateEnvelope ();
        }


        public virtual void GenerateCollisionShape (int orientations, Boolean useExisting) {
            if (useExisting && GetCollisionShape (null, null) > 0) {
                return;
            }

            foreach (var child in children) {
                child.GenerateCollisionShape (orientations, useExisting);
            }

            this.collisionShape = new CollisionShape ();

            foreach (var child in children) {
                var tra = child.CompositeTransform;
                var c   = new CollisionShape (child.collisionShape);
                c.Transform (tra);

                this.collisionShape.Merge (c);
            }

            this.collisionShape.GenerateEnvelope ();
        }


        // 自分自身と全ての子孫を格納した配列を返す。順番は必ず自分自身に近い方から順に並ぶ。
        public Node[] Descendants {
            get {
                var descendants = new List<Node> ();
                descendants.Add (this);
                foreach (var child in children) {
                    descendants.AddRange (child.Descendants);
                }
                return descendants.ToArray ();
            }
        }


        bool Intersect (Node node) {
            if (this == node) {
                return false;
            }

            return collisionShape.Intersect (node.collisionShape);
        }

        public bool Collide (int colliderScope, Node collidees, int collideeScope, bool fullTraverse, Collisions collisions) {
            if (collidees == null) {
                throw new ArgumentException ("Collidee is null");
            }
            if (FindMostRecentCommonAncestor (this, collidees) == null) {
                throw new ArgumentException ("This and collidees has no common ancestor");
            }

            var colliderNodes = Descendants.Where (x => x.collisionEnabled &&
                                                        x.GetCollisionShape (null, null) > 0 &&
                                                       (x.scope & colliderScope) != 0)
                                                        .ToList ();

            var collideeNodes = collidees.Descendants.Where (x => x.collisionEnabled &&
                                                                  x.GetCollisionShape (null, null) > 0 &&
                                                                 (x.scope & collideeScope) != 0)
                                                                  .ToList ();

            if (!fullTraverse) {
                colliderNodes.RemoveAll (node => node.SearchUpward (parent => colliderNodes.Contains (parent)) != null);
                collideeNodes.RemoveAll (node => node.SearchUpward (parent => collideeNodes.Contains (parent)) != null);
            }

            var cache = new Dictionary<Node, CollisionShape> ();
            foreach (var x in colliderNodes) {
                var colshp = new CollisionShape (x.collisionShape);
                colshp.Transform (x.GetGlobalTransform);
                cache.Add (x, colshp);
            }
            foreach (var y in collideeNodes) {
                var colshp = new CollisionShape (y.collisionShape);
                colshp.Transform (y.GetGlobalTransform);
                cache.Add (y, colshp);
            }

            foreach (var x in colliderNodes) {
                foreach (var y in collideeNodes) {
                    if (!(x == y || x.Ancestors.Contains (y) || y.Ancestors.Contains (x))) {
                        if (cache[x].Intersect (cache[y])) {
                            collisions.AddCollider (x);
                            collisions.AddCollidee (x, y);
                        }
                    }
                }
            }

            return (collisions.ColliderCount > 0) ? true : false;
        }


        public float LODResolution {
            get { return lodResolution; }
            set {
                var featuresPerUnit = value;
                if (featuresPerUnit < 0) {
                    throw new ArgumentException ("FeaturesPerUnit must be within [0,inf]");
                }
                this.lodResolution = featuresPerUnit;
                this.children.Sort ((x, y) => (int)(x.LODResolution - y.LODResolution));
            }
        }



        public int AnimateLOD (int time, Camera camera, float resX, float resY) {

            // 未実装

            return 0;
        }


        public virtual void SelectLOD (Camera camera, float resX, float resY) {
            // do nothing
        }


        public override Object3D[] References {
            get {
                var q = new List<Object3D> ();
                q.AddRange (base.References);
                q.AddRange (children);
                foreach (var child in children) {
                    q.AddRange (child.References);
                }
                q.RemoveAll (x => x == null);
                return q.ToArray ();
            }
        }


        public Transform GetTransformTo (Node target) {
            if (target == null) {
                throw new ArgumentNullException ("Target is null");
            }

            Node ancestor = FindMostRecentCommonAncestor (this, target);
            if (ancestor == null) {
                return null;
            }

            var t1  = this.GetTransformToAncestor (ancestor);
            var t2  = target.GetTransformToAncestor (ancestor);
            t2.Invert ();
            t2.PostMultiply (t1);

            return t2;
        }

        // 自分から先祖への変換行列を取得する。
        // 微妙に名前が気に入らない...
        Transform GetTransformToAncestor (Node ancestor) {
            var node = this;
            var g = new Transform ();
            do {
                if (node == ancestor)
                    break;
                var t = node.CompositeTransform;
                t.PostMultiply (g);
                g = t;
            } while ((node = node.parent) != null);
            return g;
        }

        // グローバル座標(world)への変換行列を取得する
        public Transform GetGlobalTransform {
            get {
                var node = this;
                var g = CompositeTransform;
                while ((node = node.parent) != null) {
                    var t = node.CompositeTransform;
                    t.PostMultiply (g);
                    g = t;
                }
                return g;
            }
        }


        static Node FindMostRecentCommonAncestor (Node a, Node b) {
            return a.Ancestors.Intersect (b.Ancestors).FirstOrDefault ();
        }

        // 自分自身と全ての祖先を格納した配列を返す。順番は必ず自分自身に近い方から順に並ぶ。
        public Node[] Ancestors {
            get {
                var node      = this;
                var ancestors = new List<Node> ();
                do {
                    ancestors.Add (node);
                } while ((node = node.parent) != null);
                return ancestors.ToArray ();
            }
        }



        public override int Animate (int time) {

            base.Animate (time);

            var animatedAlpha          = new float[1];
            var animatedBoundingBox    = new float[6];
            var animatedBoundingSphere = new float[4];
            var animatedCollisionShape = new float[26];
            var animatedPickability    = new float[1];
            var animatedVisibility     = new float[1];
            var value                  = new float[26];
            var animated               = new Dictionary<int, bool> (6);

            for (var i=0; i < AnimationTrackCount; i++) {
                var track = AnimationTrack[i];
                var ch    = AnimationTrackChannel[i];
                if (track.Controller == null || !track.Controller.Active (time)) {
                    continue;
                }
                var property = track.TargetProperty;
                var weight   = track.Controller.Weight;

                animated[property] = true;
                switch (property) {
                    case M3G.AnimationTrack.Alpha: {
                        track.Sample (time, ch, value);
                        animatedAlpha[0] = value[0] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.BoundingBox: {
                        track.Sample (time, ch, value);
                        animatedBoundingBox[0] = value[0] * weight;
                        animatedBoundingBox[1] = value[1] * weight;
                        animatedBoundingBox[2] = value[2] * weight;
                        animatedBoundingBox[3] = value[3] * weight;
                        animatedBoundingBox[4] = value[4] * weight;
                        animatedBoundingBox[5] = value[5] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.BoundingSphere: {
                        track.Sample (time, ch, value);
                        animatedBoundingSphere[0] = value[0] * weight;
                        animatedBoundingSphere[1] = value[1] * weight;
                        animatedBoundingSphere[2] = value[2] * weight;
                        animatedBoundingSphere[3] = value[3] * weight;
                        break;
                    }
                    case M3G.AnimationTrack.CollisitonShape: {
                        // 未実装
                        break;
                    }
                    case M3G.AnimationTrack.Pickability: {
                        track.Sample (time, ch, value);
                        animatedPickability[0] = value[0] * weight;

                        break;
                    }
                    case M3G.AnimationTrack.Visibility: {
                        track.Sample (time, ch, value);
                        animatedVisibility[0] = value[0] * weight;
                        break;
                    }
                }// switch
            }  // for

            if (animated[M3G.AnimationTrack.Alpha]) {
                this.alphaFactor = animatedAlpha[0].Clamp (0, 1);
            }
            if (animated[M3G.AnimationTrack.BoundingBox]) {
                this.boundingBox = animatedBoundingBox;
            }
            if (animated[M3G.AnimationTrack.BoundingSphere]) {
                this.boundingSphere = animatedBoundingSphere;
            }
            if (animated[M3G.AnimationTrack.CollisitonShape]) {
                // 未実装(CollisionShape)
            }
            if (animated[M3G.AnimationTrack.Pickability]) {
                this.pickingEnabled = (animatedPickability[0] >= 0.5f) ? true : false;
            }
            if (animated[M3G.AnimationTrack.Visibility]) {
                this.renderingEnabled = (animatedVisibility[0] >= 0.5f) ? true : false;
            }

            return 0;
        }

        /**
         * 上方向に検索し条件に一致する最初のNodeを返す(検索対象に自分自信は含まれない)。
         * */
        Node SearchUpward (Predicate<Node> match) {
            var node = this;
            while ((node = node.parent) != null) {
                if (match (node)) {
                    return node;
                }
            }
            return null;
        }


        // BVが存在して範囲外ならtrueを返す(カリングされた)
        // それ以外ならfalseを返す(さらに詳細な衝突判定が必要と言うこと)
        internal bool CulledByBoundingVolume (float[] pos, float[] dir) {
            var ray    = new Ray (new Vector3 (pos), new Vector3 (dir));
            var culled = false;

            if (boundingSphere != null) {
                var center = new Vector3 (boundingSphere[0], boundingSphere[1], boundingSphere[2]);
                var radius = boundingSphere[3];
                var bsph   = new BoundingSphere (center, radius);
                culled = !ray.Intersects (ref bsph);
            }
            if (!culled && boundingBox != null) {
                var min  = new Vector3 (boundingBox[0], boundingBox[2], boundingBox[4]);
                var max  = new Vector3 (boundingBox[1], boundingBox[3], boundingBox[5]);
                var bbox = new BoundingBox (min, max);
                culled =  !ray.Intersects (ref bbox);
            }

            return culled;
        }

        /**
         * このノードと子ノードのレイとの交差判定を行う。
         * 衝突があったらtrueを返す
         * */
        internal virtual bool Intersect (uint scope, float[] pos, float[] dir, RayIntersection ri) {
            var minDistance = float.MaxValue;
            var intersected = false;
            var result      = new RayIntersection ();

            foreach (var child in children) {
                var tra = this.GetTransformTo (child);
                tra.TransformVectors3 (pos, true);
                tra.TransformVectors3 (dir, false);

                var hit = child.Intersect (scope, pos, dir, result);
                if (hit && result.Distance < minDistance) {
                    result.CopyTo (ri);
                    minDistance = result.Distance;
                    intersected = true;
                }
            }
            return intersected;
        }

        public Transform ModelViewProjectionMatrix {
            get {
                var wld = SearchUpward (x => x is World) as World;
                var cam = wld.ActiveCamera;
                var modelView = GetTransformTo (cam);
                var proj = cam.ProjectionTransform;
                proj.PostMultiply (modelView);

                return proj;
            }
        }

        public Transform ModelViewMatrix {
            get {
                var wld = SearchUpward (x => x is World) as World;
                return GetTransformTo (wld.ActiveCamera);
            }
            
        }

        #endregion

    }
}
